package hu.debrecen.adastra.card.service;

import hu.debrecen.adastra.card.repository.domain.Account;
import hu.debrecen.adastra.card.repository.domain.BotAccount;
import hu.debrecen.adastra.card.service.PlayerStatisticsService.PlayerStatisticsEvent;
import hu.debrecen.adastra.card.service.domain.Card;
import hu.debrecen.adastra.card.service.domain.Deck;
import hu.debrecen.adastra.card.service.domain.PlayerState;
import hu.debrecen.adastra.card.service.domain.Round;
import hu.debrecen.adastra.card.service.exceptions.FullGameException;
import hu.debrecen.adastra.card.service.impl.ZsirozasCardGame.Action;
import hu.debrecen.adastra.card.util.TimeUtil;
import hu.debrecen.adastra.card.web.domain.AbstractGameStateVO;
import hu.debrecen.adastra.card.web.domain.GameSummaryVO;
import hu.debrecen.adastra.card.web.domain.HandCardVO;

import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Collections2;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

public abstract class AbstractGame<C extends Card> {

	private static final long NON_ACTION_EXPIRE_TIME = 1000 * 60 * 1; // 1m

	private static final Logger LOGGER = LoggerFactory.getLogger(AbstractGame.class);

	private final String gameId;

	private final Account owner;

	private final Map<Account, PlayerState> playerStates = Maps.newLinkedHashMap();

	private long lastActionTime;

	private long lastStateFetchTime;

	private final List<Round> rounds = Lists.newLinkedList();

	private final int maxPlayerCount;

	private final Deck<C> deck;

	protected final PlayerStatisticsService playerStatisticsService;

	protected final ListMultimap<Account, AbstractGameStateVO> actionGameState = LinkedListMultimap.create();

	public AbstractGame(final String gameId, final Account owner, final Deck<C> deck, final int maxPlayerCount, final PlayerStatisticsService playerStatisticsService) {
		this.gameId = gameId;
		this.owner = owner;
		this.deck = deck;
		this.maxPlayerCount = maxPlayerCount;
		this.playerStatisticsService = playerStatisticsService;

		PlayerState playerState = this.getDefaultPlayerState();
		AbstractGame.LOGGER.debug("Player({}) created game. Card:{}", owner, playerState.getHand());
		this.playerStates.put(owner, playerState);

		playerStatisticsService.start(owner).increaseCreateGameCount().save();
	}

	public abstract void call(final Account player, final String type, final String suit);

	protected abstract AbstractGameStateVO getGameState(Account player);

	protected abstract PlayerState getDefaultPlayerState();

	public AbstractGameStateVO getState(final Account player) {

		AbstractGameStateVO gameState = null;

		if (!this.isFull()) {
			gameState = new WaitingGameStateVO(Action.WAITING);
		} else {

			if (this.actionGameState.get(player).isEmpty()) {
				AbstractGame.LOGGER.trace("Return state type of game for player({})", player);
				gameState = this.getGameState(player);
			} else {
				AbstractGame.LOGGER.debug("Return action state type of game for player({})", player);
				gameState = Iterables.getFirst(this.actionGameState.get(player), null);
				this.actionGameState.get(player).remove(gameState);
			}

		}

		PlayerStatisticsEvent statistics = this.playerStatisticsService.start(player);
		if (gameState.isFinished()) {
			statistics.increaseFinishedGameCount();
		}
		statistics.save();

		this.updateLastStateFetch(player);
		return gameState;
	}

	public synchronized AbstractGame<C> join(final Account player) throws FullGameException {

		Preconditions.checkState(!this.getOwner().equals(player), "You cannot join to game which has been created by you.");
		Preconditions.checkState(!this.getAccounts().contains(player), "You are joined.");

		if (this.isFull()) {
			throw new FullGameException("The game is full.");
		}

		PlayerState playerState = this.getDefaultPlayerState();
		AbstractGame.LOGGER.debug("Player({}) joined to game. Card:{}", player, playerState.getHand());
		this.playerStates.put(player, playerState);

		if (this.isFull()) {
			this.startNewRound(this.owner);
		}

		this.playerStatisticsService.start(player).increaseJoinGameCount().save();
		this.updateLastActionTime(this.owner);
		this.updateLastStateFetch(player);

		return this;
	}

	protected List<Account> getCallOrder(final Account player) {

		AbstractGame.LOGGER.trace("Compute call order.Player: {} , Players: {}", player, this.playerStates.keySet());

		Preconditions.checkState(this.playerStates.keySet().contains(player), "Player does not bind this game.");

		List<Account> callOrder = Lists.newArrayList(player);

		AbstractGame.LOGGER.trace("Add first player({}) to call order.", player.getUsername());

		Iterator<Account> accountIterator = Iterators.cycle(this.playerStates.keySet());
		Iterators.indexOf(accountIterator, Predicates.equalTo(player));

		for (int i = 0; i < (this.maxPlayerCount - 1); i++) {
			Account nextAccount = accountIterator.next();
			AbstractGame.LOGGER.trace("Add player({}) to call order.", nextAccount.getUsername());
			callOrder.add(nextAccount);
		}

		AbstractGame.LOGGER.trace("call order: {}", callOrder);
		return callOrder;
	}

	public synchronized boolean isFull() {
		return this.getAccounts().size() == this.maxPlayerCount;
	}

	public Account getOwner() {
		return this.owner;
	}

	protected void endRound(final Account player, final List<Card> table) {
		AbstractGame.LOGGER.debug("Player won the round({}). End of round({}).", this.rounds.size(), this.rounds.size());
		this.getPlayerState(player).getTaken().addAll(table);
	}

	protected void startNewRound(final Account player) {
		List<Account> callOrder = this.getCallOrder(player);
		AbstractGame.LOGGER.debug("Start new round. Call order: {}", callOrder);
		this.rounds.add(new Round(callOrder));
	}

	protected void putCardOnTheTable(final Account player, final Card card) {
		AbstractGame.LOGGER.trace("Player({}) puts a card({}) on table in {}. round", new Object[] { player.getUsername(), card, this.rounds.size() });
		this.getLastRound().putOnTable(player, card);

		AbstractGame.LOGGER.trace("Player({}) removes a card({}) from hand in {}. round", new Object[] { player.getUsername(), card, this.rounds.size() });
		this.getPlayerState(player).removeCard(card);
	}

	protected Card findCardInPlayerHand(final Account player, final String type, final String suit) {

		List<Card> cards = this.getPlayerState(player).getHand();

		Optional<Card> optional = Iterables.tryFind(cards, new Predicate<Card>() {

			@Override
			public boolean apply(final Card card) {
				return card.getType().equalsIgnoreCase(type) && card.getSuit().equalsIgnoreCase(suit);
			}

		});

		Preconditions.checkState(optional.isPresent(), "Card must be in your hand!");

		return optional.get();
	}

	protected Round getLastRound() {
		return this.rounds.get(this.rounds.size() - 1);
	}

	protected int getRoundCount() {
		return this.rounds.size();
	}

	public String getGameId() {
		return this.gameId;
	}

	protected Set<Account> getAccounts() {
		return this.playerStates.keySet();
	}

	public Deck<C> getDeck() {
		return this.deck;
	}

	protected PlayerState getPlayerState(final Account account) {
		return this.playerStates.get(account);
	}

	protected void updateLastActionTime(final Account player) {
		this.lastActionTime = TimeUtil.getCurrentTime();
	}

	protected void updateLastStateFetch(final Account player) {
		if (!(player instanceof BotAccount)) {
			this.lastStateFetchTime = TimeUtil.getCurrentTime();
		}
	}

	public boolean isFinished() {

		// Deck is empty and there is not anybody who has a card in hand
		return this.isFull() && this.deck.isEmpty() && !Iterables.any(this.playerStates.values(), new Predicate<PlayerState>() {

			@Override
			public boolean apply(final PlayerState playerState) {
				return !playerState.getHand().isEmpty();
			}

		});

	}

	// Game is orphan if there was not any call from the client side
	public boolean isOrphanaGame() {
		long elapsedTime = TimeUtil.getCurrentTime() - this.lastStateFetchTime;
		AbstractGame.LOGGER.debug("Game({}) - elapsed time:{}", this.gameId, TimeUtil.format(elapsedTime));
		return elapsedTime > AbstractGame.NON_ACTION_EXPIRE_TIME;
	}

	public boolean isExpiredByLastAction() {
		return this.isFull() && ((TimeUtil.getCurrentTime() - this.lastActionTime) > (1000 * 9));
	}

	public AbstractGameStateVO getLastState(final Account player) {
		if (this.actionGameState.get(player).isEmpty()) {
			return this.getGameState(player);
		} else {
			return Iterables.getLast(this.actionGameState.get(player));
		}
	}

	public GameSummaryVO finish(final Account account) {

		Iterable<Account> otherPlayers = Iterables.filter(this.playerStates.keySet(), Predicates.not(Predicates.equalTo(account)));

		Account opponent = Iterables.getFirst(otherPlayers, null);

		Preconditions.checkNotNull(opponent, "Opponent player must not be null.");

		return new GameSummaryVO(this.getPlayerState(account).getTaken(), this.getPlayerState(opponent).getTaken());
	}

	public Set<Account> getPlayers() {
		return this.playerStates.keySet();
	}

	protected List<HandCardVO> getAllValidCard(final List<Card> hand) {
		return Lists.newLinkedList(Collections2.transform(hand, new Function<Card, HandCardVO>() {

			@Override
			public HandCardVO apply(final Card card) {
				return new HandCardVO(card, true);
			}

		}));
	}

	@Override
	public String toString() {
		return "AbstractGame [gameId=" + this.gameId + ", players=" + this.playerStates.keySet() + "]";
	}
}
